package disjunction.timing
{
	import flash.events.TimerEvent;
	import flash.utils.Timer;
	import flash.utils.getTimer;
	
	/**
	 * Provides a timer that returns time delta and accumulated time,
	 * and restricts delta if it exceeds some threshold value (see
	 * Gaffer's article, "Fix Your Timestep").
	 */
	
	public class SimulationTimer extends Timer
	{
		/** The base timestep value. */
		protected var _baseUpdateIntervalMs:Number = 0;	
		/** The upper limit to how much time is considered to have passed in the simulation. */
		protected var _updateTimeoutMs:int;
		/** This cycle's time since app start. */
		protected var _thisTimeMs:int;
		/** Last cycle's time since app start. */
		protected var _lastTimeMs:int;
		/** The amount of time passed since last cycle. */
		protected var _timeDeltaMs:int;
		/** The current amount of time in the accumulator, awaiting consumption. */
		protected var _timeAccumulatedMs:int = 0;
		/** The rate at which ticks are fired, the multiplies the base timestep value. */
		protected var _multiplier:Number = 1;
		
		public function SimulationTimer(delay:Number = 0, repeatCount:int = 0, updateTimeoutMs:uint = 0):void
		{
			super(delay, repeatCount);
			_baseUpdateIntervalMs = delay;
			if (_updateTimeoutMs < 0)
				throw new ArgumentError("Must be zero (no timeout) or positive value.");
			_updateTimeoutMs = updateTimeoutMs;
			_lastTimeMs = getTimer();
			
			//tick() will always be the first listener called --
			//see docs for addEventListener priority.
			addEventListener(TimerEvent.TIMER, tick);
		}
		
		/**
		 * (abstract) Calculate actual time passed, add to accumulated time while restricting
		 * if necessary.
		 * Works roughly along the lines of Gaffer's "Fix your Timestep" article.
		 */
		public function tick(e:TimerEvent):void
		{
			_thisTimeMs = getTimer();
			_timeDeltaMs = _thisTimeMs - _lastTimeMs;
			_lastTimeMs = _thisTimeMs;
			
			//in case app loses focus or processor time, restrict the delta.
			if (hasTimeout)
				if (_timeDeltaMs > _updateTimeoutMs)
					_timeDeltaMs = _updateTimeoutMs; 

			_timeAccumulatedMs += _timeDeltaMs; 
		}
		
		public function set updateTimeoutMs(value:Number):void
		{
			if (!isNaN(value) && 
				value != Number.POSITIVE_INFINITY &&
				value != Number.NEGATIVE_INFINITY &&
				value > 0)
				_updateTimeoutMs = value;
		}
		
		public function get hasTimeout():Boolean
		{
			return _updateTimeoutMs > 0;
		}
		
		/** 
		 * A workaround to the fact that the base class will run timer as fast
		 * as possible if delay is left as 0!
		 */
		public override function start():void
		{
			if (!isNaN(delay) && delay > 0)
				super.start();
		}
		
		public function get timeDeltaMs():int
		{
			return _timeDeltaMs;
		}
		
		public function get timeAccumulatedMs():int
		{
			return _timeAccumulatedMs;
		}
		
		public function consumeInterval():Boolean
		{
			if (timeAccumulatedMs >= updateIntervalMs)
			{
				_timeAccumulatedMs -= updateIntervalMs;
				return true;
			}
			return false;
		}
		
		public function get multiplier():Number
		{
			return _multiplier;
		}
		
		public function set multiplier(value:Number):void
		{
			//invalid argument error is handled by base class. 
			//TODO how about NaN? is it handled?
			super.delay = _baseUpdateIntervalMs * value;
		}
		
		public function set baseUpdateIntervalMs(value:Number):void
		{
			/*
			if (!isNaN(value) && 
				value != Number.POSITIVE_INFINITY &&
				value != Number.NEGATIVE_INFINITY &&
				value > 0)
			*/
			//invalid argument error is handled by base class.
			_baseUpdateIntervalMs = value;
			super.delay = _baseUpdateIntervalMs * multiplier;
		}
		public function get baseUpdateIntervalMs():Number
		{
			return _baseUpdateIntervalMs;
		}
		
		public function get updateIntervalMs():int
		{
			return delay;
			//return _baseUpdateIntervalMs * _multiplier; //NB cast from Number in superclass
		}
		
		//This could be avoided if composition was favoured over inheritance,
		//however we extend rather than compose here, for speed/precision purposes.
		public override function set delay(value:Number):void
		{
			throw new Error("This method is unsupported.");
		}
	}
}